/**********************************************************\
|                                                          |
|                          hprose                          |
|                                                          |
| Official WebSite: http://www.hprose.com/                 |
|                   http://www.hprose.org/                 |
|                                                          |
\**********************************************************/

/**********************************************************\
 *                                                        *
 * BytesIO.js                                             *
 *                                                        *
 * hprose BytesIO for HTML5.                              *
 *                                                        *
 * LastModified: Nov 18, 2016                             *
 * Author: Ma Bingyao <andot@hprose.com>                  *
 *                                                        *
\**********************************************************/

(function (hprose, undefined) {
    'use strict';

    var toBinaryString = hprose.toBinaryString;

    var _EMPTY_BYTES = new Uint8Array(0);
    var _INIT_SIZE = 1024;

    function writeInt32BE(bytes, p, i) {
        bytes[p++] = i >>> 24 & 0xFF;
        bytes[p++] = i >>> 16 & 0xFF;
        bytes[p++] = i >>> 8  & 0xFF;
        bytes[p++] = i        & 0xFF;
        return p;
    }

    function writeInt32LE(bytes, p, i) {
        bytes[p++] = i        & 0xFF;
        bytes[p++] = i >>> 8  & 0xFF;
        bytes[p++] = i >>> 16 & 0xFF;
        bytes[p++] = i >>> 24 & 0xFF;
        return p;
    }

    function writeString(bytes, p, str) {
        var n = str.length;
        for (var i = 0; i < n; ++i) {
            var codeUnit = str.charCodeAt(i);
            if (codeUnit < 0x80) {
                bytes[p++] = codeUnit;
            }
            else if (codeUnit < 0x800) {
                bytes[p++] = 0xC0 | (codeUnit >> 6);
                bytes[p++] = 0x80 | (codeUnit & 0x3F);
            }
            else if (codeUnit < 0xD800 || codeUnit > 0xDFFF) {
                bytes[p++] = 0xE0 | (codeUnit >> 12);
                bytes[p++] = 0x80 | ((codeUnit >> 6) & 0x3F);
                bytes[p++] = 0x80 | (codeUnit & 0x3F);
            }
            else {
                if (i + 1 < n) {
                    var nextCodeUnit = str.charCodeAt(i + 1);
                    if (codeUnit < 0xDC00 && 0xDC00 <= nextCodeUnit && nextCodeUnit <= 0xDFFF) {
                        var rune = (((codeUnit & 0x03FF) << 10) | (nextCodeUnit & 0x03FF)) + 0x010000;
                        bytes[p++] = 0xF0 | (rune >> 18);
                        bytes[p++] = 0x80 | ((rune >> 12) & 0x3F);
                        bytes[p++] = 0x80 | ((rune >> 6) & 0x3F);
                        bytes[p++] = 0x80 | (rune & 0x3F);
                        ++i;
                        continue;
                    }
                }
                throw new Error('Malformed string');
            }
        }
        return p;
    }

    function readShortString(bytes, n) {
        var charCodes = new Array(n);
        var i = 0, off = 0;
        for (var len = bytes.length; i < n && off < len; i++) {
            var unit = bytes[off++];
            switch (unit >> 4) {
            case 0:
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
            case 6:
            case 7:
                charCodes[i] = unit;
                break;
            case 12:
            case 13:
                if (off < len) {
                    charCodes[i] = ((unit & 0x1F) << 6) |
                                    (bytes[off++] & 0x3F);
                    break;
                }
                throw new Error('Unfinished UTF-8 octet sequence');
            case 14:
                if (off + 1 < len) {
                    charCodes[i] = ((unit & 0x0F) << 12) |
                                   ((bytes[off++] & 0x3F) << 6) |
                                   (bytes[off++] & 0x3F);
                    break;
                }
                throw new Error('Unfinished UTF-8 octet sequence');
            case 15:
                if (off + 2 < len) {
                    var rune = (((unit & 0x07) << 18) |
                                ((bytes[off++] & 0x3F) << 12) |
                                ((bytes[off++] & 0x3F) << 6) |
                                (bytes[off++] & 0x3F)) - 0x10000;
                    if (0 <= rune && rune <= 0xFFFFF) {
                        charCodes[i++] = (((rune >> 10) & 0x03FF) | 0xD800);
                        charCodes[i] = ((rune & 0x03FF) | 0xDC00);
                        break;
                    }
                    throw new Error('Character outside valid Unicode range: 0x' + rune.toString(16));
                }
                throw new Error('Unfinished UTF-8 octet sequence');
            default:
                throw new Error('Bad UTF-8 encoding 0x' + unit.toString(16));
            }
        }
        if (i < n) {
            charCodes.length = i;
        }
        return [String.fromCharCode.apply(String, charCodes), off];
    }

    function readLongString(bytes, n) {
        var buf = [];
        var charCodes = new Array(0x8000);
        var i = 0, off = 0;
        for (var len = bytes.length; i < n && off < len; i++) {
            var unit = bytes[off++];
            switch (unit >> 4) {
            case 0:
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
            case 6:
            case 7:
                charCodes[i] = unit;
                break;
            case 12:
            case 13:
                if (off < len) {
                    charCodes[i] = ((unit & 0x1F) << 6) |
                                    (bytes[off++] & 0x3F);
                    break;
                }
                throw new Error('Unfinished UTF-8 octet sequence');
            case 14:
                if (off + 1 < len) {
                    charCodes[i] = ((unit & 0x0F) << 12) |
                                   ((bytes[off++] & 0x3F) << 6) |
                                   (bytes[off++] & 0x3F);
                    break;
                }
                throw new Error('Unfinished UTF-8 octet sequence');
            case 15:
                if (off + 2 < len) {
                    var rune = (((unit & 0x07) << 18) |
                                ((bytes[off++] & 0x3F) << 12) |
                                ((bytes[off++] & 0x3F) << 6) |
                                (bytes[off++] & 0x3F)) - 0x10000;
                    if (0 <= rune && rune <= 0xFFFFF) {
                        charCodes[i++] = (((rune >> 10) & 0x03FF) | 0xD800);
                        charCodes[i] = ((rune & 0x03FF) | 0xDC00);
                        break;
                    }
                    throw new Error('Character outside valid Unicode range: 0x' + rune.toString(16));
                }
                throw new Error('Unfinished UTF-8 octet sequence');
            default:
                throw new Error('Bad UTF-8 encoding 0x' + unit.toString(16));
            }
            if (i >= 0x7FFF - 1) {
                var size = i + 1;
                charCodes.length = size;
                buf.push(String.fromCharCode.apply(String, charCodes));
                n -= size;
                i = -1;
            }
        }
        if (i > 0) {
            charCodes.length = i;
            buf.push(String.fromCharCode.apply(String, charCodes));
        }
        return [buf.join(''), off];
    }

    function readString(bytes, n) {
        if (n === undefined || n === null || (n < 0)) { n = bytes.length; }
        if (n === 0) { return ['', 0]; }
        return ((n < 0xFFFF) ?
                readShortString(bytes, n) :
                readLongString(bytes, n));
    }

    function readStringAsBytes(bytes, n) {
        if (n === undefined) { n = bytes.length; }
        if (n === 0) { return [_EMPTY_BYTES, 0]; }
        var i = 0, off = 0;
        for (var len = bytes.length; i < n && off < len; i++) {
            var unit = bytes[off++];
            switch (unit >> 4) {
            case 0:
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
            case 6:
            case 7:
                break;
            case 12:
            case 13:
                if (off < len) {
                    off++;
                    break;
                }
                throw new Error('Unfinished UTF-8 octet sequence');
            case 14:
                if (off + 1 < len) {
                    off += 2;
                    break;
                }
                throw new Error('Unfinished UTF-8 octet sequence');
            case 15:
                if (off + 2 < len) {
                    var rune = (((unit & 0x07) << 18) |
                                ((bytes[off++] & 0x3F) << 12) |
                                ((bytes[off++] & 0x3F) << 6) |
                                (bytes[off++] & 0x3F)) - 0x10000;
                    if (0 <= rune && rune <= 0xFFFFF) {
                        i++;
                        break;
                    }
                    throw new Error('Character outside valid Unicode range: 0x' + rune.toString(16));
                }
                throw new Error('Unfinished UTF-8 octet sequence');
            default:
                throw new Error('Bad UTF-8 encoding 0x' + unit.toString(16));
            }
        }
        return [bytes.subarray(0, off), off];
    }

    function pow2roundup(x) {
        --x;
        x |= x >> 1;
        x |= x >> 2;
        x |= x >> 4;
        x |= x >> 8;
        x |= x >> 16;
        return x + 1;
    }

    function BytesIO() {
        var a = arguments;
        switch (a.length) {
        case 1:
            switch (a[0].constructor) {
            case Uint8Array:
                this._bytes = a[0];
                this._length = a[0].length;
                break;
            case BytesIO:
                this._bytes = a[0].toBytes();
                this._length = a[0].length;
                break;
            case String:
                this.writeString(a[0]);
                break;
            case Number:
                this._bytes = new Uint8Array(a[0]);
                break;
            default:
                this._bytes = new Uint8Array(a[0]);
                this._length = this._bytes.length;
                break;
            }
            break;
        case 2:
            this._bytes = new Uint8Array(a[0], a[1]);
            this._length = a[1];
            break;
        case 3:
            this._bytes = new Uint8Array(a[0], a[1], a[2]);
            this._length = a[2];
            break;
        }
        this.mark();
    }

    Object.defineProperties(BytesIO.prototype, {
        _bytes: { value: null, writable: true },
        _length: { value: 0, writable: true },
        _wmark: { value: 0, writable: true },
        _off: { value: 0, writable: true },
        _rmark: { value: 0, writable: true },
        _grow: { value: function(n) {
            var bytes = this._bytes;
            var required = this._length + n;
            var size = pow2roundup(required);
            if (bytes) {
                size *= 2;
                if (size > bytes.length) {
                    var buf = new Uint8Array(size);
                    buf.set(bytes);
                    this._bytes = buf;
                }
            }
            else {
                size = Math.max(size, _INIT_SIZE);
                this._bytes = new Uint8Array(size);
            }
        } },
        length: { get: function() { return this._length; } },
        capacity: { get: function() {
            return this._bytes ? this._bytes.length : 0;
        } },
        position: { get: function() { return this._off; } },
        // returns a view of the the internal buffer.
        bytes: { get : function() {
            return (this._bytes === null) ?
                    _EMPTY_BYTES :
                    this._bytes.subarray(0, this._length);
        } },
        buffer: { get : function() {
            if (this._bytes === null) {
                return _EMPTY_BYTES.buffer;
            }
            if (this._bytes.buffer.slice) {
                return this._bytes.buffer.slice(0, this._length);
            }
            var buf = new Uint8Array(this._length);
            buf.set(this._bytes.subarray(0, this._length));
            return buf.buffer;
        } },
        mark: { value: function() {
            this._wmark = this._length;
            this._rmark = this._off;
        } },
        reset: { value: function() {
            this._length = this._wmark;
            this._off = this._rmark;
        } },
        clear: { value: function() {
            this._bytes = null;
            this._length = 0;
            this._wmark = 0;
            this._off = 0;
            this._rmark = 0;
        } },
        writeByte: { value: function(b) {
            this._grow(1);
            this._bytes[this._length++] = b;
        } },
        writeInt32BE: { value: function(i) {
            if ((i === (i | 0)) && (i <= 2147483647)) {
                this._grow(4);
                this._length = writeInt32BE(this._bytes, this._length, i);
                return;
            }
            throw new TypeError('value is out of bounds');
        } },
        writeUInt32BE: { value: function(i) {
            if (((i & 0x7FFFFFFF) + 0x80000000 === i) && (i >= 0)) {
                this._grow(4);
                this._length = writeInt32BE(this._bytes, this._length, i | 0);
                return;
            }
            throw new TypeError('value is out of bounds');
        } },
        writeInt32LE: { value: function(i) {
            if ((i === (i | 0)) && (i <= 2147483647)) {
                this._grow(4);
                this._length = writeInt32LE(this._bytes, this._length, i);
                return;
            }
            throw new TypeError('value is out of bounds');
        } },
        writeUInt32LE: { value: function(i) {
            if (((i & 0x7FFFFFFF) + 0x80000000 === i) && (i >= 0)) {
                this._grow(4);
                this._length = writeInt32LE(this._bytes, this._length, i | 0);
                return;
            }
            throw new TypeError('value is out of bounds');
        } },
        write: { value: function(data) {
            var n = data.byteLength || data.length;
            if (n === 0) { return; }
            this._grow(n);
            var bytes = this._bytes;
            var length = this._length;
            switch (data.constructor) {
            case ArrayBuffer:
                bytes.set(new Uint8Array(data), length);
                break;
            case Uint8Array:
                bytes.set(data, length);
                break;
            case BytesIO:
                bytes.set(data.bytes, length);
                break;
            default:
                for (var i = 0; i < n; i++) {
                    bytes[length + i] = data[i];
                }
                break;
            }
            this._length += n;
        } },
        writeAsciiString: { value: function(str) {
            var n = str.length;
            if (n === 0) { return; }
            this._grow(n);
            var bytes = this._bytes;
            var l = this._length;
            for (var i = 0; i < n; ++i, ++l) {
                bytes[l] = str.charCodeAt(i);
            }
            this._length = l;
        } },
        writeString: { value: function(str) {
            var n = str.length;
            if (n === 0) { return; }
            // A single code unit uses at most 3 bytes.
            // Two code units at most 4.
            this._grow(n * 3);
            this._length = writeString(this._bytes, this._length, str);
        } },
        readByte: { value: function() {
            if (this._off < this._length) {
                return this._bytes[this._off++];
            }
            return -1;
        } },
        readInt32BE: { value: function() {
            var bytes = this._bytes;
            var off = this._off;
            if (off + 3 < this._length) {
                var result = bytes[off++] << 24 |
                             bytes[off++] << 16 |
                             bytes[off++] << 8  |
                             bytes[off++];
                this._off = off;
                return result;
            }
            throw new Error('EOF');
        } },
        readUInt32BE: { value: function() {
            var value = this.readInt32BE();
            if (value < 0) {
                return (value & 0x7FFFFFFF) + 0x80000000;
            }
            return value;
        } },
        readInt32LE: { value: function() {
            var bytes = this._bytes;
            var off = this._off;
            if (off + 3 < this._length) {
                var result = bytes[off++]       |
                             bytes[off++] << 8  |
                             bytes[off++] << 16 |
                             bytes[off++] << 24;
                this._off = off;
                return result;
            }
            throw new Error('EOF');
        } },
        readUInt32LE: { value: function() {
            var value = this.readInt32LE();
            if (value < 0) {
                return (value & 0x7FFFFFFF) + 0x80000000;
            }
            return value;
        } },
        read: { value: function(n) {
            if (this._off + n > this._length) {
                n = this._length - this._off;
            }
            if (n === 0) { return _EMPTY_BYTES; }
            return this._bytes.subarray(this._off, this._off += n);
        } },
        skip: { value: function(n) {
            if (this._off + n > this._length) {
                n = this._length - this._off;
                this._off = this._length;
            }
            else {
                this._off += n;
            }
            return n;
        } },
        // the result is an Uint8Array, and includes tag.
        readBytes: { value: function(tag) {
            var pos = Array.indexOf(this._bytes, tag, this._off);
            var buf;
            if (pos === -1) {
                buf = this._bytes.subarray(this._off, this._length);
                this._off = this._length;
            }
            else {
                buf = this._bytes.subarray(this._off, pos + 1);
                this._off = pos + 1;
            }
            return buf;
        } },
        // the result is a String, and doesn't include tag.
        // but the position is the same as readBytes
        readUntil: { value: function(tag) {
            var pos = Array.indexOf(this._bytes, tag, this._off);
            var str = '';
            if (pos === this._off) {
                this._off++;
            }
            else if (pos === -1) {
                str = readString(this._bytes.subarray(this._off, this._length))[0];
                this._off = this._length;
            }
            else {
                str = readString(this._bytes.subarray(this._off, pos))[0];
                this._off = pos + 1;
            }
            return str;
        } },
        readAsciiString: { value: function(n) {
            if (this._off + n > this._length) {
                n = this._length - this._off;
            }
            if (n === 0) { return ''; }
            return toBinaryString(this._bytes.subarray(this._off, this._off += n));
        } },
        // n is the UTF16 length
        readStringAsBytes: { value: function(n) {
            var r = readStringAsBytes(this._bytes.subarray(this._off, this._length), n);
            this._off += r[1];
            return r[0];
        } },
        // n is the UTF16 length
        readString: { value: function(n) {
            var r = readString(this._bytes.subarray(this._off, this._length), n);
            this._off += r[1];
            return r[0];
        } },
        // returns a view of the the internal buffer and clears `this`.
        takeBytes: { value: function() {
            var buffer = this.bytes;
            this.clear();
            return buffer;
        } },
        // returns a copy of the current contents and leaves `this` intact.
        toBytes: { value: function() {
            return new Uint8Array(this.bytes);
        } },
        toString: { value: function() {
            return readString(this.bytes, this._length)[0];
        } },
        clone: { value: function() {
            return new BytesIO(this.toBytes());
        } },
        trunc: { value: function() {
            this._bytes = this._bytes.subarray(this._off, this._length);
            this._length = this._bytes.length;
            this._off = 0;
            this._wmark = 0;
            this._rmark = 0;
        } }
    });

    function toString(data) {
        /* jshint -W086 */
        if (data.length === 0) { return ''; }
        switch(data.constructor) {
        case String: return data;
        case BytesIO: data = data.bytes;
        case ArrayBuffer: data = new Uint8Array(data);
        case Uint8Array: return readString(data, data.length)[0];
        default: return String.fromCharCode.apply(String, data);
        }
    }

    Object.defineProperty(BytesIO, 'toString', { value: toString });

    hprose.BytesIO = BytesIO;

})(hprose);
